React 文档
导读与全书目录表:见同目录 01-React 入门:简介·特性与设计范式.md 开头。
在 react 中定义和修改 state
在函数组件中
- 定义 state:在函数内的最外层,使用 useState 创建响应式数据和修改响应式数据的方法
- 修改 state:调用第二参数修改响应式数据
- state dom 更新后执行:借助 useEffect 实现依赖绑定
js
import React, { useState, useEffect, useRef } from 'react';
function MyComponent() {
// 1、定义 基础类型 状态
const [count, setCount] = useState(0);
// 1、定义 对象类型 状态
const [user, setUser] = useState({
name: '张三',
age: 20,
email: 'zhangsan@example.com',
});
const countRef = useRef(null);
const increment = () => {
setCount(count + 1); // 2、修改 基础类型 状态
// 2、修改 对象类型 状态
setUser({
...user,
age: 21,
});
// 2、修改状态(传入函数):可获取之前值,返回新值
setCount((prevCount) => prevCount - 1);
};
// 3、当 count 变化且 DOM 更新后执行
useEffect(() => {
console.log('DOM已更新,count:', count, 'ref:', countRef.current);
}, [count]);
return <div ref={countRef}>{count}</div>;
}在类组件中
- 定义 state:在 constructor 中,使用 this.state 定义状态树,在状态树上定义响应式数据
- 修改 state:调用 this.setState() 并以对象属性的方式传入改变的最新状态
- state dom 更新后执行: this.setState() 的第二个参数为更新后回调
js
class ClassComponent extends React.Component {
constructor(props) {
super(props);
// 1、在状态树上定义状态
this.state = {
count: 0,
name: 'John',
};
}
incrementCount = () => {
// 2、修改状态 & 第二个参数为 dom 更新后执行回调
this.setState(
{
count: this.state.count + 1,
},
() => {
console.log('count 已更新为:', this.state.count);
}
);
// 2、修改状态(传入函数):可获取之前值,返回新值,第二个参数为 dom 更新后执行回调
this.setState(
(prevState) => ({ count: prevState.count + 1 }),
() => {
console.log('count 已更新为:', this.state.count);
}
);
};
}setState 执行机制
- setState 第一个参数可以是对象,可以是函数,第二个参数为回调函数在状态更新后执行
- 当 setState 第一个参数为函数时:函数的第一个参数为最新 state,第二个参数为最新 props
- 异步/同步与批量:多数情况下可理解为「更新会批量提交、不会立刻读到最新 state」。React 18 起默认 automatic batching,在
setTimeout、Promise、原生事件里多次setState也常合并为一次渲染;React 17 及以前在这些脱离合成事件的场景里更容易出现「看起来像同步刷新」的行为。需要强制同步提交时用flushSync - 合并批量更新:在一个方法中多次执行 setState 效果是合并覆盖、不同的合并,相同的只有最后一个生效,相当于 Object.assign(可能造成同一个属性更新缺失)
state 增删改
使用 Immer、Immutable.js 或扩展运算符 ...,避免直接修改 state。
- 函数 state 操作
js
// 错误方式:直接修改原数组
arr.push(newItem);
setArr(arr);
// 正确方式:创建新数组
setArr([...arr, newItem]); // 添加
setArr(arr.filter(item => item.id !== id)); // 删除
setArr(arr.map(item => item.id === id ? {...item, done: true} : item)); // 更新- 对象 state 操作
js
// 错误方式:直接修改原对象
obj.name = 'new name';
setObj(obj);
// 正确方式:创建新对象
setObj({...obj, name: 'new name'});- hook 函数操作
js
// 避免依赖旧状态的多个更新互相覆盖
setCount(prev => prev + 1);
// 数组更新更安全
setItems(prev => [...prev, newItem]);React setState 执行机制
回答
shell
setState 是 React 类组件中用于更新状态、触发视图重新渲染的核心 API,其设计遵循 不可变数据 + 批量更新 原则
# setState 执行机制分为状态更新和渲染两个阶段
状态更新阶段:React 会将 setState 的更新请求入队并批量处理;子树是否跳过更新由 **`shouldComponentUpdate` / `PureComponent` / `React.memo`** 等在 **reconcile** 阶段决定(不是「入队时」单独一步)。
渲染触发阶段:状态更新完成后,React 会重新执行 render 方法,生成新的虚拟 DOM,通过 Diff 算法对比新旧虚拟 DOM,最终只更新变化的真实 DOM。setState 执行机制拆解
shell
# 首先:异步执行(非绝对)
默认场景(合成事件 / 生命周期):setState 是 “异步” 的 —— 调用后不会立即更新 this.state,也不会立即触发渲染。React 会将多次 setState 合并,待当前同步代码执行完毕后,批量更新状态并触发一次渲染,目的是减少 DOM 操作、提升性能。
// 示例:合成事件中 setState 异步
handleClick = () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 输出旧值,未立即更新
};
**React 18 注意**:定时器 / Promise / 原生事件中的多次 `setState` 通常仍会被 **自动批处理**,`this.state` 未必在同步代码里立刻变成新值;与 React 17 及以前的直觉不同。需要「立刻提交 DOM」时用 `flushSync` 包裹。
// 示例:定时器里连续 setState(18 下多会合并为一次渲染)
componentDidMount() {
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 }); // 若未批处理/未用函数式,仍可能只基于旧 state 合并
console.log(this.state.count); // 18 下多为旧值;用回调第二个参数或 componentDidUpdate 读最新值更可靠
}, 0);
}
# 其次:批量更新
React 会维护一个更新队列,在同一事件循环中,多次调用 setState 会被合并为一次更新:
- 对象式 setState:后调用的会覆盖前调用的同字段(浅合并)
this.setState({ count: this.state.count + 1 });
this.setState({ count: this.state.count + 1 });
// 最终 count 只加 1(因为两次读取的都是旧 state)
- 函数式 setState:接收 prevState(上一次的真实状态)和 props,能保证状态更新的连续性,避免批量更新导致的覆盖问题。
this.setState(prevState => ({ count: prevState.count + 1 }));
this.setState(prevState => ({ count: prevState.count + 1 }));
// 最终 count 加 2(每次读取的是最新的 prevState)
# 最后:状态更新与渲染
状态更新:React 将更新入队并批量处理;子树是否 `render` 由 **`shouldComponentUpdate` / `PureComponent` / `React.memo`** 等在 reconcile 阶段决定。
渲染触发:状态更新完成后,React 会重新执行 render 方法,生成新的虚拟 DOM,通过 Diff 算法对比新旧虚拟 DOM,最终只更新变化的真实 DOM。
# 拓展
- setState 的第二个参数:可选的回调函数,会在状态更新完成、组件渲染后执行,可用于获取最新状态(替代异步场景下的立即读取)。
this.setState({ count: 1 }, () => {
console.log(this.state.count); // 输出 1,已更新
});
- React 18 变化:React 18 中默认开启 automatic batching(自动批量更新),即使在定时器、Promise 等场景,也会默认批量处理 setState,若需取消批量更新,可使用 ReactDOM.flushSync。
import { flushSync } from 'react-dom';
setTimeout(() => {
flushSync(() => {
this.setState({ count: 1 }); // 同步更新,立即触发渲染
});
}, 0);setState 是同步还是异步?更新机制?如何强制获取更新后状态?
回答
shell
# 总结
以 **批量更新** 理解更准确:**React 18** 在多数场景(含 `setTimeout` / Promise / 原生事件)默认 **automatic batching**;**React 17 及以前**在脱离合成事件的场景里更容易出现「非批量、像同步」的表现。读取最新状态用 **setState 回调**、**componentDidUpdate** 或 **`useEffect` 依赖**。
# 更新机制、原理
批量场景:React 合并同一轮中的更新,减少渲染。调用 `setState` 后立刻读 `this.state` 往往仍是旧值。
需强制同步刷新:使用 **`react-dom` 的 `flushSync`**(会打断批处理,慎用)。
# 获取更新后状态
setState 的第二个参数(回调函数):setState ({count: 1}, () => console.log (this.state.count))。
useEffect 监听状态变化:useEffect (() => { ... }, [count])。React:不可变数据(Immutability)
为什么强调不可变
React 依赖 引用变化 判断是否需要更新(PureComponent、React.memo、hooks 依赖数组)。若 原地修改(mutate) 对象或数组,引用不变,容易导致 跳过更新 或 难以调试的状态异常。
常见错误写法
js
state.items.push(newItem); // 禁止:修改原数组
setState(state); // 引用未变推荐写法
js
setItems([...items, newItem]);
setUser({ ...user, name: 'next' });深层结构可使用 immer、结构化克隆 或拆分状态降低嵌套。
与 Redux 的关系
Redux 要求 reducer 纯函数 + 不可变返回,便于时间旅行与预测状态;RTK 内置 immer 可写风格 reducer。
Immutable.js
持久化数据结构适合超大列表与复杂变更,但学习与互操作成本较高;多数项目 展开运算符 + Immer 已足够。
React render 方法原理?执行时机?
回答
shell
# render 方法的核心原理:
类组件的 render 是组件的核心生命周期方法,且必须是纯函数 —— 无副作用、不修改 state、相同输入(props/state)返回相同虚拟 DOM,这是 React 可预测性的基础;
render 返回的 React 元素是虚拟 DOM 的抽象描述,React 会经过 Reconciliation(调和)过程,通过 Fiber 架构(React 16+)将虚拟 DOM 转换为 Fiber 节点,再通过 Diff 算法对比 Fiber 树,计算出需要更新的部分(Effect List),最后通过 commit 阶段将差异应用到真实 DOM;
ReactDOM.render(React 18 前)是根组件的挂载入口,React 18 后替换为 createRoot,核心逻辑一致,但 createRoot 支持并发渲染,能优先级调度渲染任务,避免长任务阻塞主线程。
# render 的执行时机(区分类组件和函数组件):
- 类组件:
挂载阶段:constructor 之后、componentDidMount 之前执行一次;
更新阶段:state 变化(setState 即使值未变,默认触发,可通过 shouldComponentUpdate 拦截)、props 变化、父组件渲染、Context 变化、forceUpdate 调用;这里要注意,setState 是异步的,批量更新时会合并多次 setState,最终只触发一次 render;
- 函数组件:
没有 render 方法,组件本身就是渲染函数,重新执行等价于类组件的 render 调用;
触发重执行的时机包括:state/useReducer 状态更新、父组件重渲染(可通过 React.memo 浅比较 props 优化)、useContext 上下文更新、useCallback/useMemo 依赖变化(间接触发);
另外,React 18 的并发特性下,函数组件可能出现 “中断渲染”(比如高优先级任务插队),但由于是纯函数,不会产生副作用,这也是纯函数设计的优势。
最后补充优化点:无论是类组件还是函数组件,不必要的 render 会带来性能损耗,类组件可通过 shouldComponentUpdate 或 PureComponent,函数组件通过 React.memo + useCallback/useMemo 来避免无意义的重渲染,核心是减少不必要的虚拟 DOM 对比。细化
shell
# 核心原理:
类组件必须实现的核心方法,本质是一个纯函数、它的作用不是直接渲染 DOM,而是返回一个 React 元素(虚拟 DOM,JSX 语法糖编译后的产物),描述当前组件应该呈现的 UI 结构。
React 会将这个虚拟 DOM 与上一次的虚拟 DOM 进行对比(Diff 算法),计算出最小更新量,再将差异部分更新到真实 DOM 中(Reconciliation 调和过程)。
# 流程图解
触发 render -> 执行 render -> 生成虚拟 dom -> React 使用 diff 算法 对比新旧 DOM -> 计算最小更新量 -> 更新真实 dom
# 执行时机
初始化阶段:组件挂载时(componentDidMount 前),render 会执行一次,生成初始虚拟 DOM 并渲染真实 DOM。
更新阶段(核心,需重点说):
① 组件自身 state 变化(调用 this.setState(),即使 state 值未变,默认也会触发 render,可通过 shouldComponentUpdate 阻止);
② 父组件重新渲染时,子组件默认会继续 reconcile;若 props 浅比较相等且子为 **`React.memo`**,或类子 **`shouldComponentUpdate` 返回 `false`**,可跳过子 `render`
③ 组件接收的 props 发生变化;
④ 调用 this.forceUpdate()(强制触发,跳过 shouldComponentUpdate 检查);
⑤ 上下文(Context)的值发生变化,且组件消费了该上下文。
初始化挂载必执行,更新阶段由 state/props/ 父组件 / 上下文变化触发,函数组件无 render 方法,组件重执行等价于 render 调用;
# 函数组件(无 render 方法,但渲染逻辑等价于 return 部分)执行时机
初始化挂载时执行一次;
组件内 useState/useReducer 的状态更新;
父组件重新渲染(可通过 React.memo 优化);
依赖的 useContext 上下文更新;
useCallback/useMemo 的依赖项变化(间接触发)React Refs 理解与使用场景
回答
shell
Refs 是 React 提供的直接访问 DOM / 类组件实例的方式,useRef(函数组件)/createRef(类组件)创建,通过 current 属性访问目标;
# 注意事项
Refs 不能用在函数组件上(因为函数组件无实例),若需给函数组件绑定 ref,需用 forwardRef 转发 ref;
函数组件中,useRef 比 createRef 更高效(createRef 每次渲染都会创建新的 ref 对象)react 类组件 super
为什么要使用 super(props) 而不是 super()?
直接调用父类构造函数,不传递 props 给父类构造函数,在构造函数中可以通过 this.props 访问 props,官方不推荐 因为,在构造函数中 this.props 将是 undefined 在构造函数外(如 render 方法)仍可以访问 this.props,因为 React 会在之后自动设置
在 React 16.9 之前,如果需要在构造函数中访问 this.props,必须使用 super(props) 现代 React 版本中,React 会在构造函数调用后自动设置 props,所以技术上两者都可以工作 但为了代码清晰性和一致性,通常建议使用 super(props)
总结
- react 是基于 ES6 Class 类的,所以如果使用 constructor 函数,必须使用 super
- 无论有没有 constructor 函数,在 render 方法中,this.props 都是可以使用的,这是因为 React 内部实现了, 在构造函数之外自动设置了 this.props,但是不建议 super() 代替 super(props)
react 中 super 和 super(props)的区别
回答
shell
super() 和 super(props) 的核心区别在于是否能在组件实例的 constructor 中访问到 props
在 ES6 的类中,super 是子类调用父类构造函数的关键字。React 类组件本质是继承了 React.Component
super():父类构造函数没收到 props,所以在 constructor 内部 this.props 是 undefined;
但 render 等其他生命周期里 this.props 是正常的(React 后续会把 props 挂载到实例上)。
super(props):父类构造函数收到 props 并立即挂载到 this 上,所以 constructor 内就能直接用 this.props。
最后,无论是否需要在 constructor 中用 props,都建议写 super(props),避免潜在的边界问题,代码也更规范。
# 注意
1. 只要手动定义 constructor,第一步必须调用 super()(否则报错)。
2. 简化写法:不写 constructor 时的默认行为
如果你的组件不需要手动写 constructor,React 会默认帮你调用 super(props)
等价于写了 constructor(props) { super(props); }
class MyComponent extends React.Component {
render() {
return <div>{this.props.name}</div>;
}
}
# 拓展
在 React 16 之前的部分版本中,如果只写 super(),即使在 constructor 外的生命周期(如 componentWillMount)中,this.props 也可能出现短暂的 undefined;而 super(props) 能避免这个问题(现在 React 已修复,但最佳实践仍建议传 props)
constructor() 是组件的构造函数,只要手动写了 constructor,必须首先调用 super(),否则会报错(因为子类实例化时必须先完成父类的初始化)
React.Component 的构造函数本身会接收 props 参数,并把它挂载到实例上(即 this.props)